package org.dtrevien.yuml.parser;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import org.dtrevien.yuml.model.Cardinality;
import org.dtrevien.yuml.model.Member;
import org.dtrevien.yuml.model.Param;

/**
 * YUML File parser
 * While reading a file, the parser will throws event on the handler
 * 
 * @author davidtrevien
 *
 */
public class YumlReader {

	/**
	 * The handler which receives the events
	 */
	protected YumlEventListener yumlHandler;
	
	/**
	 * Temporary class name (Left member)
	 */
	protected String tempClass0;

	/**
	 * Temporary class name (Right member)
	 */
	protected String tempClass1;

	/**
	 * Temporary class name (Current working)
	 */
	protected String tempClass;
	
	/**
	 * Flag, true on direction set
	 */
	protected boolean directionSet = false;

	public void setHandler(YumlEventListener handler) {
		this.yumlHandler = handler;
	}

	/**
	 * Start parse routine
	 * 
	 * @param f
	 * @throws IOException if file does not exist
	 */
	public void parse(final File f) throws IOException {
		parseInternal(f);
	}

	private void parseInternal(final File input) throws IOException {

		if (input == null) {
			System.err.println("File is null");
			return;
		}

		this.yumlHandler.startYUML();

		String text = read(input);
		readParse(text);

		this.yumlHandler.endYUML();
	}

	private String read(final File f) throws IOException {
		StringBuilder text = new StringBuilder();
		String NL = System.getProperty("line.separator");
		Scanner scanner = new Scanner(new FileInputStream(f), "UTF-8");
		try {
			while (scanner.hasNextLine()) {
				text.append(scanner.nextLine() + NL);
			}
		} finally {
			scanner.close();
		}
		return text.toString();
	}

	private void readParse(final String bf) throws IOException {
		String[] lines;
		lines = bf.split("\\],");
		for (String line : lines) {
			if (!maybeComment(line))
				maybeOp(line);
			this.yumlHandler.onNewLineEvent();
		}
	}

	private void maybeOp(final String l) {
		String line = new String(l.trim());
		int f0, f1, f2, f3;
		String str = "";
		//[Customer]<>1-orders 0..*>[Order]
		//f0       f1               f0    f1
		//          f2
		if (line.startsWith("[")) {
			f0 = line.indexOf("[")+1;
			f1 = line.indexOf("]");
			str = line.substring(f0, f1);
			this.yumlHandler.onClassEvent(getClassName(str));
			this.tempClass0 = getClassName(str);
			this.tempClass = this.tempClass0;
			extractClassData(str);
			
			//[Customer]<>1-orders 0..*>[Order]
			line = line.substring(f1+1).trim();
			
			f2 = line.indexOf("[")+1;
			if (line.contains("]"))
				f3 = line.indexOf("]");
			else
				f3 = line.length();
			str = line.substring(f2, f3);
			this.yumlHandler.onClassEvent(getClassName(str));
			this.tempClass1 = getClassName(str);
			this.tempClass = this.tempClass1;
			extractClassData(str);
			
			line = l.substring(f1+1,f2+f1+1).replace("[", "").replace("]", "").trim();
			examineLink(line.trim());
		}
	}
	
	private String getClassName(String classMemeber) {
		String className = classMemeber;
		String[] data;
		if (classMemeber.contains("|")) {
			data = classMemeber.split("\\|");
			className = data[0];
		}
		return className;
	}
	
	/**
	 * Extract data from class member.
	 * throw event to the handler with class attributes and class methods
	 * 
	 * @param str the string containing class | attributs | methods OR class | attributes OR class | methods OR class
	 * @return the class name
	 */
	private String extractClassData(String str) {
		String className = str;
		String[] data;
		if (str.contains("|")) {
			data = str.split("\\|");
			className = data[0];
			//if data[1] dose not contain (, this is an attribute list. Else, it is a method list
			if (!data[1].contains("(")) { //Attributes
				extractAttributes(data[1]);
			}
			else {//Method
				extractMethods(data[1]);
			}
			
			//If data[2] contains (, this is a method list
			if (data.length >= 3 && data[2].contains("(")) {
				extractMethods(data[2]);
			}
		}
		return className;
	}
	
	private String isFirstCharIsVisibilityFlag(String s) {
		if (s.trim().isEmpty()) return null;
		String b = s.substring(0, 1);
		if (b.equals("+") || b.equals("-") || b.equals("#") || b.equals("~")) return b;
		return null;
	}

	/**
	 * Extract attributes from the given string (format attr;attr1;attr2...)
	 * @param string
	 */
	private void extractAttributes(String string) {
		String[] attributes;
		attributes = string.split(";");
		for (String a : attributes) {
			if (a.trim().isEmpty()) continue;
			
			String[] data = a.split(":");
			String visib = isFirstCharIsVisibilityFlag(data[0]);
			if (visib != null) {
				//cut the first char (because it is a flag)
				data[0] = data[0].substring(1);
			}
			else {
				visib = "+";
			}
			
			if (data.length>=2)
				this.yumlHandler.onClassAttributeEvent(this.tempClass, data[0], data[1], visib);
			else
				this.yumlHandler.onClassAttributeEvent(this.tempClass, data[0], "", visib);
		}
		
	}
	
	/**
	 * Extracts methods from the given string (format mth0();mth1();mth2()...)
	 * @param string
	 */
	private void extractMethods(String string) {
		String[] methods, tParameters;
		String parameters;
		List<Param> params = new ArrayList<Param>();
		String name, type;
		
		// +Login() : void;+Logout() : void;+checkPwd(password : String) : boolean
		methods = string.split(";");
		for (String m : methods) {
			params = new ArrayList<Param>();
			// +Login() : void
			// +Logout() : void
			// +checkPwd(password : String) : boolean
			String[] data = m.split("[\\)][ ]*[:]");
			// +Login()
			// void
			
			//Check visibility
			String visib = isFirstCharIsVisibilityFlag(data[0]);
			if (visib != null) {
				//cut the first char (because it is a flag)
				data[0] = data[0].substring(1);
			}
			else {
				visib = "+";
			}
			// Login()
			// void
			
			//set Params
			parameters = data[0].substring(data[0].indexOf("(")+1);
			tParameters = parameters.split(",");
			for (String p : tParameters) {
				if (!p.trim().isEmpty()) {
					String[] pContent = p.replace(")", "").replace("(", "").trim().split(":");
					if (pContent.length >= 2) {
						Param e = new Param( pContent[1].trim(), pContent[0].trim());
						params.add(e);
					}
				}
			}
			
			//set Name
			name = data[0].substring(0, data[0].indexOf("("));
			
			//set Type
			type = (data.length >= 2) ? data[1] : "void";
			
			
			//Send event
			this.yumlHandler.onClassMethodEvent(this.tempClass, name, type, params, visib);
		}
	}

	private String examineStartLink(final String link) {
		String l = new String(link);
		l = l.trim();
		
		if (l.startsWith("<>")) {
			this.yumlHandler.onAggregationEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(2);
		}
		else if (l.startsWith("++")) {
			this.yumlHandler.onCompositionEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(2);
		}
		else if (l.startsWith("+")) {
			this.yumlHandler.onAggregationEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(1);
		}
		else if (l.startsWith("-.-")) {
			this.yumlHandler.onImplementationEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(3);
		}
		else if (l.startsWith("-")) {
			this.directionSet = false;
			l = l.substring(1);
		}
		else if (l.startsWith("<")) {
			this.yumlHandler.onBidirectionalEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(1);
		}
		else if (l.startsWith("^")) {
			this.yumlHandler.onInheritanceEvent(this.tempClass0, this.tempClass1);
			this.directionSet = true;
			l = l.substring(1);
		}
		
		if (!this.directionSet && l.endsWith(">")) 
			this.yumlHandler.onDirectionalEvent(this.tempClass0, this.tempClass1);
		
		return l;
	}
	
	/**
	 * Use some methods to analyse the link
	 * 
	 * @param link
	 */
	private void examineLink(final String link) {
		String l = new String(link);
		l = l.trim();
		l = examineStartLink(link);
		System.err.println("###"+l);
		l = examineLabel(l, Member.RIGHT);
		l = examineCardinality(l);
		l = escapeMinus(l);
		l = examineLabel(l, Member.LEFT);
		l = examineCardinality(l);
	}

	private String escapeMinus(String link) {
		String l = new String(link);
		l = l.trim();
		if (l.startsWith("-")) l = l.substring(1);
		return l;
	}

	private String examineLabel(String link, Member member) {
		String l = new String(link);
		l = l.trim();
		//Retrieve the label
		StringBuilder sb = new StringBuilder();
		if (l.matches("^[A-Za-z]+.*")) {
			char[] c = l.toCharArray();
			for (int i = 0; i < c.length; i++) {
				if (new String(""+c[i]).matches("[A-Za-z]")) {
					sb.append(c[i]);
				}
				else break;
			}
		}
		//If a label exist, throw an onLabelEvent
		if (sb.length() > 0) {
			this.yumlHandler.onLabelsEvent(sb.toString(), member);
			l = l.substring(sb.toString().length());
		}
		return l;
	}

	private String examineCardinality(String link) {
		String l = new String(link), cs;
		Cardinality c;
		l = l.trim();
		if (l.length() <= 0 || l.startsWith("-") || l.startsWith(">")) return l;
		if (l.matches("^[01]\\.\\.[01\\*].*")) {
			cs = l.substring(0,4);
			if (cs.equals("0..1")) {
				c = Cardinality.zero_one;
			}
			else {// if (cs.equals("0..*")) {
				c = Cardinality.zero_n;
			}
			this.yumlHandler.onCardinalityEvent(c, this.tempClass0, this.tempClass1);
			l = l.substring(4);
		}
		else {
			cs = l.substring(0,1);
			if (cs.equals("1")) {
				c = Cardinality.one;
			}
			else {// if (cs.equals("*")) {
				c = Cardinality.n;
			}
			this.yumlHandler.onCardinalityEvent(c, this.tempClass0, this.tempClass1);
			l = l.substring(1);
		}
		return l;
	}

	private boolean maybeComment(final String line) {
		if (line.trim().startsWith("[note:")) {
			this.yumlHandler.onNoteEvent(line.replace("note:", "").replace("[", "").replace("]", "").trim());
			return true;
		}
		return false;
	}

}
